# coding=utf-8
from random import triangular, uniform

from neatHealthScripts.bedcore.client import ClientSystem, clientApi, compFactory, levelId, localPlayer
from neatHealthScripts.bedcore.common import mcEnum
from neatHealthScripts.client.config import ClientConfig
from neatHealthScripts.client.data import config
from neatHealthScripts.modInfo import ModInfo


class MainSystem(ClientSystem):
    ENTITY_FILTER = {
        'all_of': [
            {'test': 'is_visible', 'subject': 'other'},
            {'test': 'is_sneaking', 'subject': 'other', 'value': False}
        ]
    }

    gameComp = compFactory.CreateGame(levelId)

    def __init__(self, namespace, systemName):
        super(MainSystem, self).__init__(namespace, systemName)
        self.healthBoardMap = {}
        """:type: dict[str, HealthBoard]"""
        self.hurtBoards = set()
        """:type: set[HurtBoard]"""
        self.bannedEntities = set()
        """:type: set[str]"""
        self.aroundCheckTick = 0

    def addEventListen(self, namespace, systemName):
        self.ListenForEvent(namespace, systemName, 'LoadClientAddonScriptsAfter', self, self.LoadClientAddonScriptsAfter)
        self.ListenForEvent(namespace, systemName, 'DimensionChangeClientEvent', self, self.DimensionChangeClientEvent)
        self.ListenForEvent(namespace, systemName, 'RemoveEntityClientEvent', self, self.RemoveEntityClientEvent)
        self.ListenForEvent(ModInfo.NAME, 'main', 'EntityHurtEvent', self, self.EntityHurtEvent)
        self.ListenForEvent('Phosphorus', 'main', 'PhosphorusLoadedEvent', self, self.PhosphorusLoadedEvent)

    def Destroy(self):
        super(MainSystem, self).Destroy()
        config.persistence()

    def Update(self):
        self.checkAroundEntities()
        for entity, v in self.healthBoardMap.items():
            if self.gameComp.HasEntity(entity):
                v.onUpdate()
        self.hurtBoards.difference_update({v for v in self.hurtBoards if v.onUpdate()})

    def LoadClientAddonScriptsAfter(self, args):
        """
        :type args: dict
        """
        self.sendConfigChanged()

    def DimensionChangeClientEvent(self, args):
        """
        :type args: dict
        """
        self.healthBoardMap.clear()
        self.hurtBoards.clear()
        self.bannedEntities.clear()

    def RemoveEntityClientEvent(self, args):
        """
        :type args: dict
        """
        entity = args['id']
        self.healthBoardMap.pop(entity, None)
        self.bannedEntities.discard(entity)

    def EntityHurtEvent(self, args):
        """
        :type args: dict
        """
        entity = args['entity']
        if not self.gameComp.HasEntity(entity):
            return
        self.hurtBoards.add(HurtBoard(entity, args['damage']))

    def PhosphorusLoadedEvent(self, args):
        """
        :type args: dict
        """
        comp = clientApi.CreateComponent(levelId, 'Phosphorus', 'config')
        builder = comp.GetConfigBuilder()()
        default = ClientConfig()
        builder.switch('show_health', config.showHealth, default.showHealth).callback(self.showHealthChanged)
        builder.switch('show_hurt', config.showHurt, default.showHurt).callback(self.showHurtChanged)
        builder.float('board_scale', config.boardScale, default.boardScale, 0.5, 3.0).callback(self.boardScaleChanged)
        builder.int('max_distance', config.maxDistance, default.maxDistance, 10, 50).callback(self.maxDistanceChanged)
        comp.RegisterConfig('neat_health', builder.build())

    def checkAroundEntities(self):
        self.aroundCheckTick += 1
        if self.aroundCheckTick < 2:
            return
        self.aroundCheckTick = 0
        current = set()
        if config.showHealth:
            entities = set(self.gameComp.GetEntitiesAround(localPlayer, config.maxDistance, self.ENTITY_FILTER))
            entities.difference_update({localPlayer, compFactory.CreateRide(localPlayer).GetEntityRider()})
            for entity in entities:
                if not self.gameComp.HasEntity(entity):
                    continue
                if entity in self.bannedEntities:
                    continue
                if compFactory.CreateAttr(entity).GetAttrValue(mcEnum.AttrType.HEALTH) < 0:
                    self.bannedEntities.add(entity)
                    continue
                if not self.gameComp.CanSee(localPlayer, entity, config.maxDistance, True, 180, 360):
                    continue
                current.add(entity)
        latest = set(self.healthBoardMap)
        for entity in latest - current:
            self.healthBoardMap.pop(entity)
        for entity in latest & current:
            self.healthBoardMap[entity].updateOffset()
        for entity in current - latest:
            self.healthBoardMap[entity] = HealthBoard(entity)

    def showHealthChanged(self, data):
        """
        :type data: dict
        """
        config.showHealth = data['value']

    def showHurtChanged(self, data):
        """
        :type data: dict
        """
        config.showHurt = data['value']
        self.sendConfigChanged()

    def boardScaleChanged(self, data):
        """
        :type data: dict
        """
        config.boardScale = data['value']
        self.healthBoardMap.clear()
        self.hurtBoards.clear()

    def maxDistanceChanged(self, data):
        """
        :type data: dict
        """
        config.maxDistance = data['value']
        self.sendConfigChanged()

    def sendConfigChanged(self):
        self.NotifyToServer('ConfigChangedEvent', {'config': config.toSyncData()})


class HealthBoard(object):
    _textBoardComp = compFactory.CreateTextBoard(levelId)

    def __init__(self, entity):
        """
        :type entity: str
        """
        # cache comp
        self._attrComp = compFactory.CreateAttr(entity)
        self._collisionBoxComp = compFactory.CreateCollisionBox(entity)
        self._nameComp = compFactory.CreateName(entity)
        # variable init
        self._entity = entity
        self._health = self._attrComp.GetAttrValue(mcEnum.AttrType.HEALTH)
        self._maxHealth = self._attrComp.GetAttrMaxValue(mcEnum.AttrType.HEALTH)
        self._height = self._collisionBoxComp.GetSize()[1]
        self._name = self._nameComp.GetName()
        self._identifier = compFactory.CreateEngineType(entity).GetEngineTypeStr()
        # board init
        self._board = self._textBoardComp.CreateTextBoardInWorld(self._text(), (1, 1, 1, 1), (0, 0, 0, 0))
        self._textBoardComp.SetBoardBindEntity(self._board, self._entity, (0, self._offset(), 0), (0, 0, 0))
        self._textBoardComp.SetBoardDepthTest(self._board, False)
        self._textBoardComp.SetBoardScale(self._board, (config.boardScale, config.boardScale))

    def __del__(self):
        self._textBoardComp.RemoveTextBoard(self._board)

    def onUpdate(self):
        health = self._attrComp.GetAttrValue(mcEnum.AttrType.HEALTH)
        maxHealth = self._attrComp.GetAttrMaxValue(mcEnum.AttrType.HEALTH)
        diff = False
        if self._health != health:
            self._health = health
            diff = True
        if self._maxHealth != maxHealth:
            self._maxHealth = maxHealth
            diff = True
        if not diff:
            return
        self._textBoardComp.SetText(self._board, self._text())

    def updateOffset(self):
        height = self._collisionBoxComp.GetSize()[1]
        name = self._nameComp.GetName()
        diff = False
        if self._height != height:
            self._height = height
            diff = True
        if self._name != name:
            self._name = name
            diff = True
        if not diff:
            return
        self._textBoardComp.SetBoardPos(self._board, (0, self._offset(), 0))

    def _text(self):
        return float2str(self._health) + '/' + float2str(self._maxHealth)

    def _offset(self):
        if self._identifier == 'minecraft:player':
            # 玩家
            return self._height - 0.75
        elif self._name:
            # 带名字的生物
            return self._height + 0.5 + 0.35
        else:
            # 其它生物
            return self._height + 0.5


class HurtBoard(object):
    _DELAY = 1.0 / 30.0

    _textBoardComp = compFactory.CreateTextBoard(levelId)

    def __init__(self, entity, damage):
        """
        :type entity: str
        :type damage: float
        """
        # variable init
        self._tick = int(uniform(45, 60))
        # board init
        self._board = self._textBoardComp.CreateTextBoardInWorld(float2str(damage), self._textColor(), (0, 0, 0, 0))
        self._textBoardComp.SetBoardPos(self._board, self._location(entity))
        self._textBoardComp.SetBoardDepthTest(self._board, False)
        self._textBoardComp.SetBoardScale(self._board, self._scale())

    def __del__(self):
        self._textBoardComp.RemoveTextBoard(self._board)

    def onUpdate(self):
        """
        :rtype: bool
        """
        self._tick -= 1
        self._textBoardComp.SetBoardTextColor(self._board, self._textColor())
        self._textBoardComp.SetBoardScale(self._board, self._scale())
        return self._tick < 0

    def _textColor(self):
        """
        :rtype: (float, float, float, float)
        """
        return 0.3333, 1.0, 0.3333, min(1.0, self._tick * self._DELAY)

    def _scale(self):
        """
        :rtype: (float, float)
        """
        scale = min(1.0, self._tick * self._DELAY) * config.boardScale
        return scale, scale

    def _location(self, entity):
        x, y, z = compFactory.CreatePos(entity).GetFootPos()
        w, h = compFactory.CreateCollisionBox(entity).GetSize()
        return x + triangular(-w * 0.5, w * 0.5), y + triangular(0.0, h), z + triangular(-w * 0.5, w * 0.5)


def float2str(f):
    """
    :type f: float
    :rtype: str
    """
    f = round(f, 4)
    return str(int(f)) if f.is_integer() else str(f)
